No description has been provided for this image

Hito 1: Estudio de las recomendaciones de Steam¶

Grupo 03:

  • Adolfo Arenas P.
  • Alejandro Mori A.
  • Ignacio Humire S.
  • Leonardo Rikhardsson
  • Mario Benavente C.

1. Introducción¶

1.1 ¿Qué es Steam?¶

  Steam es una plataforma de distribución digital de videojuegos, software y contenido multimedia creada por Valve Corporation, enfocada en entregar dichos contenidos para computadores con sistemas operativos Windows, macOS y Linux.

Esta aplicación garantiza un ecosistema donde jugadores de todos lados del mundo puedan interactuar entre ellos mediante foros públicos, conversaciones por grupos/comunidades e incluso chats privados con tus amigos, sin mencionar opciones como juego multijugador en línea, compartir juegos entre amigos y un entorno virtual de multijugador local (en resumen, poder jugar un multijugador local mediante una conexión a un amigo).

Sumado a esto, constantemente buscan estimular la competitividad entre sus usuarios mediante opciones como estadísticas de juego, marcador de horas jugadas, integración de logros (con la opción de añadir logros "ocultos": no se menciona cómo obtenerlos hasta que el jugador cumple los requerimientos para conseguirlo), entre otros.

Finalmente, poseen herramientas como la recomendación de juegos por usuarios, la cual permite que los jugadores puedan dar a conocer sus opiniones, dando espacio para la opinión pública, de manera que pueden recomendar o no recomendar un juego, con la opción de incluir un párrafo donde argumentar su decisión.

1.2 Motivación¶

  La industria de los videojuegos está en constante crecimiento, con un aumento significativo en el número de jugadores a nivel mundial, generando una mayor demanda y expectativas por parte de los consumidores hacia las empresas desarrolladoras de videojuegos. Esto a su vez ha provocado un aumento en la cantidad de juegos lanzados en el mercado, desde aquellas producciones de las distribuidoras más importanes del mercado (conocidas como AAA) hasta los creados por desarrolladores independientes (indies). Lo anterior puede llevar a los desarrolladores de juegos preguntarse: ¿Cómo puedo hacer que mi proyecto se venda en un mercado tan saturado?

Como grupo, creemos que es fundamental analizar previamente el sector del mercado al que un desarrollador desea dirigirse considerando el tipo de producto que desea lanzar, si posee o no una empresa que lo respalde (publisher), entre otras cosas. Por lo anterior, mediante un proyecto de minería de datos sobre DataSets de Steam, buscamos proporcionar información valiosa que permita a los desarrolladores tomar decisiones informadas y aumentar sus probabilidades de éxito de ventas al lanzar su juego en la misma plataforma.

2. Exploración de Datos¶

  Para la exploración de datos se usarán dos DataSets:

  • Steam Games Dataset: Contiene información sobre cada juego (género de juego, precio, cantidad de reviews, etc).

  • Steam Reviews Dataset 2021: Contiene información más detallada sobre las reviews de usuarios por juego, incluyendo la reseña que escribió el usuario acerca de un producto.

A pesar de que las tablas tienen distintos atributos, podemos realizar Joins para unirlas, ya que comparten el atributo app_id (ID el cual es dado por Steam y es único para cada juego). Cada destacar que dado el tamaño de este dataset, solo usaremos un cuarto de los datos, que son alrededor de 12 millones de filas.

In [1]:
# @title 2.1 Imports
from nltk.corpus import stopwords
from wordcloud import WordCloud

import matplotlib.pyplot as plt
import plotly.express as px
import pandas as pd
import numpy as np
import os
import csv
import json
import nltk
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.utils.multiclass import unique_labels
In [2]:
# Codigo para que la parte interactiva se vea en el HTML

import plotly.io as pio
pio.renderers.default='notebook'
In [3]:
# @title 2.2 Exportación de Drive
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive

Primero cargamos los datos que serán usados para nuestra exploración de datos.

In [4]:
# @title 2.3 Carga de DataFrames
df_games = pd.read_csv('/content/drive/MyDrive/CC5205/games.csv', encoding="UTF-8")
df_steam_reviews = pd.read_csv('/content/drive/MyDrive/CC5205/steam_reviews.csv', encoding="UTF-8", nrows=12043389)
In [5]:
# @title 2.4  WordCloud

# Asegurarse de que la columna 'release_date' esté en formato de fecha
df_games['release_date'] = pd.to_datetime(df_games['release_date'], errors='coerce')

# Extraer el año de la fecha de lanzamiento
df_games['year'] = df_games['release_date'].dt.year

# Filtrar los datos para eliminar aquellos sin año de lanzamiento válido
df_games = df_games.dropna(subset=['year'])

# Obtener los años únicos y ordenarlos
years = sorted(df_games['year'].unique())

# Crear una nube de palabras para cada año
for year in years:
    # Filtrar los juegos por año
    juegos_ano = df_games[df_games['year'] == year]

    # Crear un diccionario para contar las frecuencias de cada género
    genero_frecuencias = {}

    # Contar las frecuencias de cada género
    for generos in juegos_ano['genres']:
        if pd.notna(generos):  # Verificar que no sea NaN
            for genero in generos.split(','):
                genero = genero.strip()  # Quitar espacios en blanco alrededor
                if genero in genero_frecuencias:
                    genero_frecuencias[genero] += 1
                else:
                    genero_frecuencias[genero] = 1

    # Generar la nube de palabras usando las frecuencias
    wordcloud = WordCloud(width=800, height=400, background_color='white').generate_from_frequencies(genero_frecuencias)

    # Mostrar la nube de palabras
    plt.figure(figsize=(10, 5))
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.title(f'Nube de Palabras de Géneros para el Año {int(year)}')
    plt.axis('off')  # Desactivar los ejes
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

  En la figura tenemos plasmados distintos WordClouds para los géneros de videojuegos que hayan sido lanzados entre 1997 y 2025, ordenados por año de salida.

Como primera iteración, se decidió hacer un WordCloud para los juegos en general, sin ningún filtro asociado. El resultado fue un gráfico muy denso, lo que hacía difícil intentar deducir algo. Además, la forma en la que se realizó el WordCloud consideraba juegos con más de un género como géneros únicos (ej: si un juego tenía los géneros “Indie” y “Action”, el WordCloud lo mostraba como “Indie Action”, lo cual era erróneo).

Por ello se decidió separarlo por años, pues permite captar tendencias de géneros populares a lo largo del tiempo, además de identificar cuándo se popularizó un cierto género en caso de querer hacer una investigación más dirigida. Finalmente, arreglamos los géneros de cada juego para separarlos por comas, separándolos como “Indie Action” en sus componentes, “Indie” y “Action”.

De estos datos podemos ver una clara tendencia de géneros, como lo fue el surgimiento de los juegos independientes (indie) lo que se puede relacionar a la facilidad de acceso a un computador durante los últimos años en comparación a 25 años atrás, además de un aumento en el material de aprendizaje para crear videojuegos. Otro caso de estas tendencias son los juegos Early access, siendo productos no terminados que se lanzan al mercado, donde los desarrolladores trabajan junto a los jugadores para recibir feedback y arreglar errores.

In [6]:
# @title 2.5 Diagrama de Dispersión
# Crear el scatter plot
fig_scatter = px.scatter(df_games,
                         x='price',
                         y='recommendations',
                         color='metacritic_score',
                         title='Scatter Plot: Precio vs Número Total de Reseñas',
                         labels={'price': 'Precio (USD)', 'recommendations': 'Número Total de Recomendaciones'})

fig_scatter.show()

  Para este análisis se buscará una posible relación entre el precio de los juegos y la cantidad de reviews de los usuarios. La mejor herramienta para estudiar esto es un diagrama de dispersión. Además, se mostrarán los outliers para dar cuenta de situaciones particulares dentro de la plataforma.

Sumado a esto, dicho diagrama de dispersión representará la ubicación del juego en el gráfico con un punto coloreado según su puntuación en Metacritic.

Metacritic es una página que recolecta las reseñas de algunos de los mejores críticos de las industrias de la televisión, cine, videojuegos, entre otros, para luego entregar un puntaje guiado por estas. Es muy útil en este contexto revisar la puntuación que un juego puede tener pues las puntuaciones entregadas por esta página suelen influir en la opinión del público general a la hora de valorar un nuevo lanzamiento, a la vez que entrega cierta credibilidad a dicha puntuación, útil para cuando un consumidor busca comprar un juego nuevo y necesita una opinión respaldada.

Del gráfico podemos notar que todos los juegos sobre los 100 dólares en adelante tiene una puntuación alrededor de 0 ya sea por una mala puntuación de parte de metacritic o simplemente no fueron puntuados, lo anterior puede deberse al sobreprecio sobre los mismos. Por otro lado, notamos que juegos con puntuaciones altas en Metacritic suelen ser juegos de no más de 60 dólares (que suele ser el precio estándar para producciones de grandes empresas) y con al menos 10 mil reseñas. En relación a lo anterior se observa una tendencia en donde a menor precio posee un juego mayor es su número de reseñas en la plataforma, esto se debe a la facilidad que hay para acceder al producto.

In [7]:
# @title 2.6 Diagrama de Dispersión con límites

# Crear el scatter plot
fig_scatter = px.scatter(df_games,
                         x='price',
                         y='recommendations',
                         color='metacritic_score',
                         title='Scatter Plot: Precio vs Número Total de Reseñas',
                         labels={'price': 'Precio (USD)', 'recommendations': 'Número Total de Recomendaciones'},
                         range_x=[0,100],
                         range_y=[1,1000000])

fig_scatter.show()

  Se muestra el mismo gráfico que en la parte anterior pero con la eliminación de outliers. En este gráfico se muestra una clara tendencia a establecer precios en múltiplos de cinco y estar por debajo de los 30 dólares. La diferencias de precios que se observan podrían depender de la producción detrás de estos, por ejemplo: un juego con un publisher reconocido detrás tendrá, por lo general, un precio de 60 dólares; un juego hecho por desarrolladores independientes rara vez superará los 40 dólares.

In [8]:
# @title 2.7 Histograma de tiempo de juego por rango de precios

# Agrupa los datos por rango de precios
price_bins = [0, 10, 20, 30, 40, 50, 60, 70, 80]
df_games['price_range'] = pd.cut(df_games['price'], bins=price_bins)

# Calcula la media del tiempo de juego promedio para cada rango de precios
games_price_range = df_games.groupby('price_range')['average_playtime_forever'].mean().reset_index()

# Convierte el tiempo de juego de minutos a horas
games_price_range['average_playtime_forever'] = games_price_range['average_playtime_forever'] / 60

# Convierte los intervalos a cadenas
games_price_range['price_range'] = games_price_range['price_range'].astype(str)

# Crea el histograma
fig = px.bar(games_price_range, x='price_range', y='average_playtime_forever',
             title='Promedio de tiempo de juego por rango de precios',
             labels={'price_range': 'Rango de precios', 'average_playtime_forever': 'Promedio de tiempo de juego (horas)'},
             color_discrete_sequence=['orange'])

# Muestra el histograma
fig.show()

  Podemos notar que el promedio de horas de juego incrementa conforme sube el precio, alcanzando su punto máximo en los 60 dólares, que es el precio estándar máximo en la industria de los videojuegos. Esta información puede ser útil para que los desarrolladores estimen cuántas horas debería durar su juego ó cuanto podrían cobrar por él.

In [9]:
# @title 2.7 WordCloud sobre palabras presentes en reseñas de juegos del género "Farming sim"

# Descargar stopwords en inglés
nltk.download('stopwords')
english_stopwords = set(stopwords.words('english'))

# Agregar las palabras específicas a las stopwords
custom_stopwords = {'game', 'really', 'play', 'like', 'thing', 'would', 'get', 'one', 'even', 'good', 'want', "I'm", 'much', 'make', 'still'}
all_stopwords = english_stopwords.union(custom_stopwords)

df_jrpg = df_games[df_games['tags'].str.contains('Farming Sim', na=False)]
id_jrpgs = df_jrpg['app_id']

df_steam_reviews_english = df_steam_reviews[(df_steam_reviews['language'] == 'english') & (df_steam_reviews['app_id'].isin(id_jrpgs))]

def create_word_cloud(reviews, title, max_words, stopwords):

    # Remover palabras vacías que no agregan nada al gráfico
    def remove_stopwords(text):
        words = text.split()
        filtered_words = [word for word in words if word.lower() not in stopwords]
        return ' '.join(filtered_words)

    cleaned_reviews = [remove_stopwords(review) for review in reviews]

    text = ' '.join(cleaned_reviews)

    wordcloud = WordCloud(width=800, height=400, max_words=max_words, background_color='white', stopwords=stopwords).generate(text)
    plt.figure(figsize=(8, 4))
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.title(title + ' Word Cloud')
    plt.axis('off')
    plt.show()

positive_reviews = df_steam_reviews_english[df_steam_reviews_english['recommended'] == True]['review'].astype(str).sample(n=1000).tolist()
negative_reviews = df_steam_reviews_english[df_steam_reviews_english['recommended'] == False]['review'].astype(str).sample(n=1000).tolist()

create_word_cloud(positive_reviews, 'Reviews positivas para Farming simulator', 1000, all_stopwords)
create_word_cloud(negative_reviews, 'Reviews negativas para Farming simulator', 1000, all_stopwords)
[nltk_data] Downloading package stopwords to /root/nltk_data...
[nltk_data]   Unzipping corpora/stopwords.zip.
No description has been provided for this image
No description has been provided for this image

  Obteniedo los comentarios en inglés para un juego del género Farming Sim podemos armar un WordCloud para ver cuales son las palabras más usadas al momento de reseñar un juego.

Se puede observar que la reseñas positivas mencionan frecuentemente Harvest Moon y Stardew Valley, juegos los cuales se podrían utilizar por un desarrollador de videojuegos como referencias para crear juegos de este género. Por otra parte, para las reseñas negativas se aprecian palabras como "graphic" (calidad gráfica), "time", "bug", "boring", etc. Estás últimas son palabras a tomar en cuenta a la hora de trabajar ya que si se descuidan estos aspectos podrían llevar a que el producto tenga malas reseñas y con ello bajas ventas.

3. Preguntas y problemas¶

  A partir de nuestra motivación y lo encontrado en la exploración de datos, como grupo nos surgieron las siguientes preguntas que encontramos interesantes para estudiar el DataSet:

  1. ¿Qué parámetros influyen más en la cantidad de ventas totales?

  2. ¿Se puede predecir la cantidad de juegos con género "X" que habrán en un año determinado?
  3. ¿Importa la popularidad del género para aumentar las ventas?. Si haces un juego de un género popular:
  • ¿Aseguras tener un mínimo de ventas?
  • ¿Dependes más de las reviews positivas?


Hito 2¶


Pregunta 1 (Clasificadores)¶

¿Qué parámetros influyen más en la cantidad de ventas totales?¶

  Para responder a esta pregunta haremos uso de clasificadores que usen distintos pares de atributos cada uno con el objetivo de medir cual de ellos tiene mejor desempeño que el resto.

En primer lugar creamos la función run_classifier(clf, X, Y, num_test=100) la cuál se encargará de entrenar un modelo a partir de un clasificador clf para predecir la variable y en función de X. Cabe destacar que se usará el 30% del conjunto de datos para entrenar el modelo y el porcentaje restante para evaluar el desempeño del mismo. Finalmente, para medir el desempeño de cada clasificador se usará el promedio de las métricas precision, recall y f1-score.

Para la comparación de los distintos clasificadores usaremos:

  • Dummy Classifier :

    Es un clasificador muy simple ya que genera predicciones ignorando las características del input. Solo se usa como punto de comparación para clasificadores más complejos.

  • Decision Tree :

    Utiliza un arból de decisión como estructura, en donde cada nodo interno representa una decisión sobre una característica.

  • Gaussian Naive Bayes :

    Está basado en el Teorema de Bayes, el cual asume independecia entre las variables. En específico, el tipo Gaussian asume que los datós numéricos presentan una distribución normal.

  • K-Nearest Neighbors (KNN) :

    Clasifica un dato según las clases de sus K vecinos más cercanos. Suele utilizar la distancia euclidiana como métrica.

Luego de obtener los resultados, graficaremos usando una matriz de confusión los resultados del clasificador con mejor desempeño, esto con el objetivo de facilitar la visualización de los datos.

Pregunta 2 (Regresión)¶

¿Se puede predecir la cantidad de juegos con género "X" que habrán en un año determinado?¶

  Asumiendo que tiene comportamiento lineal, se comprobará si se logra obtener una buena predicción haciendo uso de regresión lineal

Preprocesamiento:

  • La columna "release_date" del DataSet de Steam contiene la fecha de salida del videojuego con el formato: "mes día, año", por lo que habría que crear una columna que contenga solamente el año de salida.

  • La columna "genres" contiene elementos de la forma: "género1, género2, género3", por lo que tendríamos que extraer solamente las filas que contengan la palabra "x" en su género.

Experimento:

  • Se utiliza la regresión lineal, donde la variable independiente es el año y la dependiente la cantidad de juegos con género "x" en ese año.

Análisis:

  • Se graficará el resultado de la regresión tomando la pendiente y el intercepto. Junto con esto también graficaremos los puntos utilizados para realizar la regresión, con el objetivo de comparar visualmente la predicción con los datos reales.

  • Se calculará el Error Absoluto Medio y el coeficiente de determinación R2 para decidir si la técnica tuvo un buen desempeño al predecir los valores.

Pregunta 3 (Clustering)¶

¿Importa la popularidad del género para aumentar las ventas?. Si haces un juego de un género popular: ¿Aseguras tener un mínimo de ventas?, ¿dependes más de las reviews positivas?¶

 

Preprocesamiento:

  • Antes de comenzar el clustering es necesario comenzar con un preprocesamiento de los datos, pues dentro del DataSet de juegos hay valores nulos y/o que no están en un formato numérico apropiado.

Experimento:

  • Tras aplicar el preprocesamiento es necesario escoger las variables que usaremos para formar los clusters, la selección de estas dependerá de lo que queramos ver que influya en la popularidad del juego. Por ejemplo, seleccionaremos género para ver agrupar por popularidad y con ello concluir cuales son los más comprados/consumidos por los usuarios.

  • Una vez elegidas las características a usar, pasaremos a elegir un algoritmo que nos permite realizar el clustering, para ello elegiremos entre K-Means, DBSCAN o Hierarchical Clustering según el que de mejor desempeño y agrupe de mejor manera los datos. Cabe destacar que también será necesario encontrar el número óptimo de clusters a formar, esto puede hacerse mediante mediante la suma de la diferencia al cuadrado de dentro los puntos de cada clusters (Sum of Squares for Errors, abreviado SSE), luego gráficar lo obtenido y usar la heurística del codo para determinar más fácilmente el óptimo.

Análisis:

  • Luego, generamos gráficos para facilitar la interpretación del conjunto de datos y con ello sacar otras medidas que nos ayuden a concluir, un ejemplo de esto puede ser una matriz de similitud o el coeficiente de Silhouette.

2. Resultado preliminares¶

Pregunta 1¶

In [10]:
from sklearn.metrics import f1_score, recall_score, precision_score
from sklearn.model_selection import train_test_split
import numpy as np

def run_classifier(clf, X, y, num_tests=100):
    metrics = {'precision': [], 'recall': [], 'f1-score': []}

    for _ in range(num_tests):
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, random_state=15, stratify=y)

        clf.fit(X_train, y_train)
        predictions = clf.predict(X_test)

        metrics['precision'].append(precision_score(y_test, predictions,average='weighted', zero_division=0))
        metrics['recall'].append(recall_score(y_test, predictions, average='weighted', zero_division=0))
        metrics['f1-score'].append(f1_score(y_test, predictions, average='weighted', zero_division=0))
    return metrics
In [11]:
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB  # naive bayes
from sklearn.neighbors import KNeighborsClassifier #kNN
from sklearn.svm import SVC  # support vector machine

c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier(max_depth=5))
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN", KNeighborsClassifier(n_neighbors=10))
#c4 = ("Support Vector Machines", SVC())

classifiers = [c0, c1, c2, c3]
In [12]:
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

#separando atributos predictivos (X) del atributo objetivo (y)
X = data_filtered[['price','metacritic_score']].values
y = data_filtered['estimated_owners'].values

results = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X, y)   # hay que implementarla en el bloque anterior.
    results[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45302464994112585
Recall promedio: 0.45279264082849524
F1-score promedio: 0.4529029216032853
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.6047336565639093
Recall promedio: 0.7040640200847325
F1-score promedio: 0.6394325825118907
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.5723978257617705
Recall promedio: 0.5142397614938021
F1-score promedio: 0.5001481621798194
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6050389965558003
Recall promedio: 0.7016318845127886
F1-score promedio: 0.635961224535507
----------------


In [13]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
import numpy as np
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

# Filtramos las clases válidas
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

# Separando atributos predictivos (X) del atributo objetivo (y)
X = data_filtered[['price', 'metacritic_score']].values
y = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=15, stratify=y)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
No description has been provided for this image
In [14]:
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X1 = data_filtered[['price','positive']].values
y1 = data_filtered['estimated_owners'].values

results1 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X1, y1)   # hay que implementarla en el bloque anterior.
    results1[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45287710098476075
Recall promedio: 0.4529111093676448
F1-score promedio: 0.45288907260828687
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7497584274632675
Recall promedio: 0.7819708143731368
F1-score promedio: 0.7573347346223646
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.2627310747470592
Recall promedio: 0.24435116899419415
F1-score promedio: 0.1579333562389433
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.7366627779636193
Recall promedio: 0.7740467597677704
F1-score promedio: 0.7487414539848498
----------------


In [15]:
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X1 = data_filtered[['price','positive']].values
y1 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X1, y1, test_size=0.30, random_state=15, stratify=y1)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
No description has been provided for this image
In [16]:
# Hacemos una copia del DataFrame original
df_games_copy = df_games.copy()

# Convertimos la columna release_date a datetime
df_games_copy['release_date'] = pd.to_datetime(df_games_copy['release_date'], errors='coerce')

# Extraemos características numéricas de la fecha
df_games_copy['year'] = df_games_copy['release_date'].dt.year
df_games_copy['month'] = df_games_copy['release_date'].dt.month
df_games_copy['day'] = df_games_copy['release_date'].dt.day

# Filtramos las clases válidas
class_counts = df_games_copy['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games_copy[df_games_copy['estimated_owners'].isin(valid_classes)]

# Eliminamos las filas con NaN en las características seleccionadas
data_filtered = data_filtered.dropna(subset=['year', 'month', 'day', 'recommendations'])

# Preparamos las características y la variable objetivo
X2 = data_filtered[['year', 'month', 'day', 'recommendations']].values
y2 = data_filtered['estimated_owners'].values

results2 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X2, y2)   # hay que implementarla en el bloque anterior.
    results2[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.4531800334376483
Recall promedio: 0.45346147811077975
F1-score promedio: 0.45331547563076335
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.616086940999507
Recall promedio: 0.6820178879648516
F1-score promedio: 0.6055770806871238
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.12177468346275867
Recall promedio: 0.2011611485956379
F1-score promedio: 0.09135724986895451
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6166905569975587
Recall promedio: 0.6709948219049114
F1-score promedio: 0.62973466092412
----------------


In [17]:
# Hacemos una copia del DataFrame original
df_games_copy = df_games.copy()

# Convertimos la columna release_date a datetime
df_games_copy['release_date'] = pd.to_datetime(df_games_copy['release_date'], errors='coerce')

# Extraemos características numéricas de la fecha
df_games_copy['year'] = df_games_copy['release_date'].dt.year
df_games_copy['month'] = df_games_copy['release_date'].dt.month
df_games_copy['day'] = df_games_copy['release_date'].dt.day

# Filtramos las clases válidas
class_counts = df_games_copy['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games_copy[df_games_copy['estimated_owners'].isin(valid_classes)]

# Eliminamos las filas con NaN en las características seleccionadas
data_filtered = data_filtered.dropna(subset=['year', 'month', 'day', 'recommendations'])

# Preparamos las características y la variable objetivo
X2 = data_filtered[['year', 'month', 'day', 'recommendations']].values
y2 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X2, y2, test_size=0.30, random_state=15, stratify=y2)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
No description has been provided for this image
In [18]:
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X3 = data_filtered[['positive','negative']].values
y3 = data_filtered['estimated_owners'].values

results3 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X3, y3)   # hay que implementarla en el bloque anterior.
    results3[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45285587058387755
Recall promedio: 0.4527251686803704
F1-score promedio: 0.4527860122528192
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7029434610039946
Recall promedio: 0.7033971442021025
F1-score promedio: 0.68788219727119
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.10356141353952113
Recall promedio: 0.2256786442805586
F1-score promedio: 0.1153237190476415
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6975135052229418
Recall promedio: 0.6999450808096661
F1-score promedio: 0.6850103852704434
----------------


In [19]:
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X3 = data_filtered[['positive','negative']].values
y3 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X3, y3, test_size=0.30, random_state=15, stratify=y3)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
No description has been provided for this image

  Nuevamente, con respecto a los clasificadores seleccionados, ignoraremos Base Dummy al ser un método básico para clasificar, presente más que nada para referencia (asegurarnos de tener un mínimo aproximado para los valores de Precision, Recall y F1-Score).

En el caso de Gaussian Naive Bayes, se puede observar un desempeño ineficiente. Este resultado se pudo haber dado por una posible dependencia entre los atributos, lo cual tendría conflictos con la metodología de la técnica usada.

Es por esto que interpretaremos y analizaremos la información obtenida programáticamente utilizando los clasificadores Decision Tree y KNN.

En el contexto del desarrollo de videojuegos, no queremos que un developer se quede con la noción de que su juego vendió más unidades de las que realmente se vendieron: si bien ambos escenarios son perjudiciales para sus decisiones futuras (tanto decirle a un developer que vendió más unidades que el monto real o que vendió menos puede alterar la forma en la que vea la situación), como grupo consideramos que es más seguro decirle que vendió menos del monto real. Esto se debe a que consideramos que el potencial de perder dinero es mucho mayor si el developer cree tener más presupuesto del disponible (en esencia, que su juego vendió más del monto real), derivando en decisiones que podrían apuntar a proyectos más entusiastas o proyectos hechos para presupuestos superiores a los que el developer se puede permitir.

Cabe destacar que lo anterior puede aplicar también para sus proyectos actuales, puesto que traer nuevo contenido a un juego o actualizarlo para mejorar la calidad actual del juego conlleva aumentar el presupuesto inicial del proyecto.

Con respecto a los datos, se observan que los atributos se encuentran en clases desbalanceadas, por lo que, para todo intento de estudio con estos clasificadores, las métricas como Accuracy, Precision, Recall y F1-Score están dando resultados dominados por la clase [0, 20000]. Como se están obteniendo datos preliminares, no nos enfocaremos en balancear las clases para finalidades de este hito, pero se considerará realizar Over/Undersampling para comparar resultados.

Además, a la hora de obtener resultados, hemos únicamente clasificado para cuatro pares de atributos, habiendo otros como "Average Playtime" (tiempo promedio de juego por los usuarios) o "User Score" (porcentaje de las reviews positivas con respecto al total). No consideramos las combinaciones escogidas como totalmente concluyentes, pero nos entrega una buena noción de qué parámetros influyen más a la hora de entregar información a un desarrollador de juegos.

En conclusión, en vista de los resultados programáticos que hemos obtenido, a un desarrollador de juegos le es más relevante la información obtenida a partir de las clases “precio del juego” y “cantidad de reseñas positivas”, información útil para hacer que su juego tenga más impacto y que más usuarios se conviertan en posibles compradores. Con esto, el desarrollador tendrá una idea más clara de si su producto fue un éxito o un fracaso en la fecha de lanzamiento.

Contribuciones de cada integrante Hito 1¶

  • Adolfo Arenas: Creación del Canva(PPT), el diseño y encargado de cargar los datos desde Google Drive. Trabajó Wordcloud de géneros e histograma.
  • Alejandro Mori: A cargo de la redacción de las preguntas y los motivos. Trabajó Wordcloud de géneros e histograma.
  • Ignacio Humire: Encargado de la limpieza de los datos, además del diseño del PPT. Analiza y comenta el scatterplot.
  • Leonardo Rikhardsson: Estuvo a cargo de la creación del WordCloud de géneros por año. Analiza y comenta el scatterplot.
  • Mario Benavente: Redactar cada parte de los análisis y Exploración de datos. Analiza y comenta el scatterplot.

Contribuciones de cada integrante Hito 2¶

  • Adolfo Arenas: Encargado del código de la pregunta 1, además de explicar la metodología de la pregunta 3.
  • Alejandro Mori: Encargado de parte del código de la pregunta 1, además de explicar la metodología de la pregunta 2.
  • Ignacio Humire: Encargado de parte del código de la pregunta 1, además analiza y comenta los resultados obtenidos.
  • Leonardo Rikhardsson: Encargado de parte del código de la pregunta 1, además de explicar la metodología de la pregunta 3.
  • Mario Benavente: Encargado de parte del código de la pregunta 1,además analiza y comenta los resultados obtenidos.

Anexos¶

Steam reviews. (2023, noviembre 9). Kaggle.com; Kaggle. https://www.kaggle.com/code/gonzafrancoandres/steam-reviews